﻿#region Using's

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Serialization;
using CommonTestingUtils;
using LightPersist.Exceptions;
using LightPersist.Metainfo;
using LightPersist.Storage;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections;
using System.IO;

#endregion

[Serializable]
public struct MyStruct
{
	public string Field1;
	string field2;
	public string Field2 { get { return field2; } set { field2 = value; } }
	//readonly int field3;
	//int field4;

	public Guid MyGuid;
	public DateTime MyDateTime;

//	public int Field
//	{
//		get { return field3; }
//	}

	//public TimeSpan MyTimeSpan;

	public MyStruct(int fld3)
	{
		Field1 = "Field1_value";
		field2 = "Field2_value";
		//field3 = fld3;
		//field4 = 10;
		MyGuid = Guid.NewGuid();
		//MyTimeSpan = DateTime.Now.TimeOfDay;
		MyDateTime = DateTime.Now;
	}
}


namespace LightPersist.XmlStorageTests
{
	public abstract class TestStorage : FixtureMsTest
	{
		protected readonly ClassMetainfo testClassMeta = new ClassMetainfo("TestClass")
		{
			PropertiesDeclared =
				{
					new PropertyMetainfo
					{
						Type = typeof(int).FullName,
						Name = "X"
					},
					new PropertyMetainfo
					{
						Type = typeof(int).FullName,
						Name = "Y"
					},
				}
		};

		protected readonly Guid testObjectID = Guid.NewGuid();
		protected LightPersistContext context;
		protected string lastConnectionString = Guid.NewGuid().ToString();
		IStorage __sut;
		protected IStorage sut
		{
			get { return __sut; }
			set
			{
				if (__sut != null)
				{
					__sut.Dispose();
				}
				__sut = value;
			}
		}
		protected ObjectState testObectState;

		protected abstract IStorage CreateStorage();

		protected virtual void ReopenStorage()
		{
			CloseStorage();
		}

		[TestInitialize]
		public void SetUp()
		{
			sut = CreateStorage();
			sut.Connect(lastConnectionString);

			context = new LightPersistContext(sut, new DomainMetainfo { DomainName = "test" });

			testObectState = new ObjectState(context)
			{
				ClassMetainfo = testClassMeta,
				Identity = testObjectID,
			};
		}

		[TestCleanup]
		public void TestStorage_TearDown()
		{
			CloseStorage();
		}

		protected void CloseStorage()
		{
			if(sut != null)
			{
				sut.Dispose();
			}
		}

		[TestMethod]
		public void Should_not_require_open_transaction_before_any_action()
		{
			CheckEveryMethodExclude(null, (methodInfo, invoke) => NotThrows(invoke, methodInfo.Name), "Begin",
			                        "Connect", "Abort");
		}

		[TestMethod]
		public void Should_require_connection_before_any_action()
		{
			CheckEveryMethodExclude(delegate { sut = CreateStorage(); },
			                        (methodInfo, invoke) => Throws(invoke, methodInfo.Name), "Connect", "Abort");

			CheckEveryMethodExclude(delegate { sut = CreateStorage(); }, (methodInfo, action) => Throws(action, methodInfo.Name), "Connect", "Abort", "Begin");

			CheckEveryMethodExclude(delegate
			{
				sut = CreateStorage();
				sut.Connect(lastConnectionString);
			}, (methodInfo, action) =>
			{
				
				NotThrows(action, methodInfo.Name);
			}, "Begin", "Connect");
		}

		[TestMethod]
		public void Should_persist_primitive_values_on_object()
		{
			testObectState.Set("X", 55);
			sut.CreateObject(testObectState);
			ReopenStorage();
			var statePersisted = PrepareNewObjectState(testObjectID);
			sut.LoadObject(statePersisted);
			AssertEqualObjectState(testObectState, statePersisted);
		}

		[TestMethod]
		public void Should_update_primitive_values_on_object()
		{
			testObectState.Set("X", 55);
			sut.CreateObject(testObectState);
			Throws(delegate { sut.CreateObject(testObectState); });
			testObectState.Set("X", 56);
			sut.UpdateObject(testObectState);
			ReopenStorage();
			var statePersisted = PrepareNewObjectState(testObjectID);
			sut.LoadObject(statePersisted);
			AssertEqualObjectState(testObectState, statePersisted);
			testObectState.Set("X", 57);
			ReopenStorage();
			sut.UpdateObject(testObectState);
			ReopenStorage();
			var statePersisted2 = PrepareNewObjectState(testObjectID);
			sut.LoadObject(statePersisted2);
			Equal(57, statePersisted2.Get("X"));
		}

		[TestMethod]
		public void Should_remove_objects()
		{
			
			sut.CreateObject(testObectState);
			Throws(() => sut.CreateObject(testObectState));
			

			// check that object exists
			Throws(() =>
			{
				
				sut.CreateObject(testObectState);
				
			});

			
			sut.DeleteObject(testObectState);
			

			// check that object removed
			Throws(delegate
			{
				try
				{
					
					var statePersisted = PrepareNewObjectState(testObjectID);
					sut.LoadObject(statePersisted);
				}
				finally
				{
					
				}
			});
			NotThrows(() =>
			{
				
				sut.CreateObject(testObectState);
				
			});
		}

		protected ObjectState PrepareNewObjectState()
		{
			return PrepareNewObjectState(Guid.Empty);
		}

		protected ObjectState PrepareNewObjectState(Guid id)
		{
			var statePersisted = new ObjectState(context)
			{
				ClassMetainfo = testClassMeta, // TODO Must be restored from xml
				Identity = default(Guid) == id ? Guid.NewGuid() : id,
			};
			return statePersisted;
		}

		[TestMethod]
		public void Should_throw_exception_on_loading_object_with_empty_id()
		{
			var persisted = PrepareNewObjectState();
			persisted.Identity = Guid.Empty;

			
			Throws(() => sut.LoadObject(persisted));
		}

		[TestMethod]
		public void Should_throw_on_inserting_object_with_empty_id()
		{
			
			testObectState.Identity = default(Guid);
			Throws<ApplicationException>(() => sut.CreateObject(testObectState));
		}

		[TestMethod]
		public void Should_throw_on_loading_missing_object()
		{
			sut.CreateObject(testObectState);
			testObectState.Identity = Guid.NewGuid();
			Throws<StorageException>(() => sut.LoadObject(testObectState));
		}

//		[TestMethod]
//		public void Should_throw_on_load_property_for_object_with_empty_id()
//		{
//			Throws<ApplicationException>(delegate
//			{
//				
//				testObectState.Set("X", 1);
//				testObectState.Identity = default(Guid);
//				sut.LoadProperty(testObectState, "X");
//			});
//		}
//
//		[TestMethod]
//		public void Should_throw_on_load_null_property()
//		{
//			
//			Throws<ArgumentNullException>(() => sut.LoadProperty(testObectState, null));
//			Throws<ArgumentNullException>(() => sut.LoadProperty(testObectState, string.Empty));
//		}

		[TestMethod]
		public void Should_throw_on_updating_object_with_empty_id()
		{
			
			testObectState.Identity = default(Guid);
			Throws<ApplicationException>(() => sut.UpdateObject(testObectState));
		}

		[TestMethod]
		public void Should_throw_on_removing_object_with_empty_id()
		{
			
			testObectState.Identity = default(Guid);
			Throws<ApplicationException>(() => sut.DeleteObject(testObectState));
		}

		[TestMethod]
		public void Should_load_current_object_property_when_it_changed_winthin_current_transaction()
		{
			
			testObectState.Set("X", 1);
			sut.CreateObject(testObectState);
			

			
			testObectState.Set("X", 2);
			sut.UpdateObject(testObectState);
			testObectState.Set("X", 55);
			sut.LoadObject(testObectState);
			
		}

		[TestMethod]
		public void Should_remove_missing_properties_form_object_on_load()
		{
			testObectState.Set("X", 1);
			testObectState.Set("Y", 2);
			
			sut.CreateObject(testObectState);
			

			testObectState.Set("Z", 3);
			
			sut.LoadObject(testObectState);
			

			NotThrows(() => Equal(2, testObectState.Get("Y")));
		}

		[TestMethod]
		public void Should_throw_on_accessing_disposed_storage()
		{
			CheckEveryMethodExclude(null, (mi, invoke) =>
			{
				sut.Dispose();
				Throws<ObjectDisposedException>(invoke, mi.Name);
			}, "Abort");
		}

		[TestMethod]
		public void Should_allow_connect_only_once()
		{
			var sut = CreateStorage();
			sut.Connect(Guid.NewGuid().ToString());
			Throws<InvalidOperationException>(() => sut.Connect(Guid.NewGuid().ToString()));
		}

		[TestMethod]
		public void Should_check_connection_string_on_null()
		{
			sut = CreateStorage();
			Throws<ArgumentNullException>(() => sut.Connect(null));
		}

		[TestMethod]
		public void Should_not_store_lists_as_separate_object()
		{
			testObectState.Set("list1", new[]
			{
				1, 2, 3
			});
			testObectState.Set("list2", new[]
			{
				"a", "b", "c"
			});

			var id1 = Guid.NewGuid();
			var id2 = Guid.NewGuid();

			testObectState.Set("list3", new[]
			{
				id1, id2,
			});
			sut.CreateObject(testObectState);
			testObectState.ClearObject();
			sut.LoadObject(testObectState);

			Equal(1, sut.GetObjectIDs().Count());

			Equal(new[]
			{
				1, 2, 3
			}, (IEnumerable)testObectState.Get("list1"));
			Equal(new[]
			{
				"a", "b", "c"
			}, (IEnumerable)testObectState.Get("list2"));
			Equal(new[]
			{
				id1, id2
			}, (IEnumerable)testObectState.Get("list3"));

		}

		[TestMethod]
		public void Should_return_all_guids_of_stored_objects()
		{
			var id1 = testObectState.Identity;
			sut.CreateObject(testObectState);
			var id2 = testObectState.Identity = Guid.NewGuid();
			sut.CreateObject(testObectState);
			var id3 = testObectState.Identity = Guid.NewGuid();
			sut.CreateObject(testObectState);
			Equivalent(new[]{id1,id2,id3}, sut.GetObjectIDs());
		}

		[TestMethod]
		public void Should_throw_on_remove_unexisting_object()
		{
			testObectState.Identity = Guid.NewGuid();
			Throws(delegate
			{
				sut.DeleteObject(testObectState);
			});
		}

		#region utils

		/*		protected Action BeginCommit
		{
			set
			{
				//sut.Begin();
				value();
				//sut.Commit();
			}
		}*/

		const BindingFlags PublicDeclared = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public;

		void CheckEveryMethodExclude(Action<MethodInfo> prepareBase, Action<MethodInfo, Action> invokeWrapper,
		                             params string[] excludeMethodNames)
		{
			foreach(MethodInfo mi in
				typeof(IStorage).GetMembers(PublicDeclared))
			{
				if(excludeMethodNames.Contains(mi.Name))
				{
					continue;
				}

				sut = CreateStorage();
				sut.Connect(lastConnectionString);

				if(prepareBase != null)
				{
					prepareBase(mi);
				}

				var paramInstances = new List<object>();
				var parameters = mi.GetParameters();
				foreach(var parameterInfo in parameters)
				{
					paramInstances.Add(GetInstance(parameterInfo.ParameterType));
				}

				switch(mi.Name)
				{
					case "DeleteObject":
					case "LoadObject":
					case "LoadProperty":
					case "UpdateObject":
						if(mi.Name == "LoadProperty")
						{
							paramInstances[1] = "X";
							testObectState.Set("X", 1);
						}
						try
						{
							sut.CreateObject(testObectState);
						}
						catch
						{
						}
						break;
				}

				invokeWrapper(mi, () =>
				{
					try
					{
						mi.Invoke(sut, paramInstances.ToArray());
					}
					catch(TargetInvocationException ex)
					{
						if(!(ex.InnerException is NotImplementedException))
						{
							throw ex.InnerException;
						}
					}
				});
			}
		}

		object GetInstance(Type type)
		{
			if(type.IsByRef)
			{
				type = typeof(ObjectState).Assembly.GetType(type.FullName.TrimEnd('&'), true, false);
			}

			if(type == typeof(ObjectState))
			{
				return testObectState;
			}
			else if(type == typeof(string))
			{
				return Guid.NewGuid().ToString();
			}
			else
			{
				return Activator.CreateInstance(type, true);
			}
		}

		protected void AssertEqualObjectState(ObjectState stateExpected, ObjectState statePersisted)
		{
			Equal(stateExpected.Identity, statePersisted.Identity);
			// Equal(stateExpected.Version, statePersisted.Version);
			var statesExpected = stateExpected.PropertiesState.ToArray();

			CheckValues(statePersisted, statesExpected);

			Equal(statesExpected.Length, statePersisted.PropertiesState.Count());
		}

		protected void CheckValues(ObjectState statePersisted,
		                           IEnumerable<KeyValuePair<string, object>> statesExpected)
		{
			foreach(var c in statesExpected)
			{
				Equal(c.Value, statePersisted.Get(c.Key));
			}
		}

		#endregion



		[TestMethod]
		public void Should_serizalize_custom_struct()
		{
			var myStruct = new MyStruct(5);
			testObectState.Set("MyField", myStruct);

			var list = new List<MyStruct>
			{
				myStruct
			};

			testObectState.Set("MyList", list);

			sut.CreateObject(testObectState);
			testObectState.ClearObject();

			ReopenStorage();
			sut.LoadObject(testObectState);

			Assert.IsNotNull(testObectState.Get("MyField"));
			var storedStruct1 = (MyStruct)testObectState.Get("MyField");
			var storedStruct2 = ((List<MyStruct>)testObectState.Get("MyList")).First();
			CompareStructs(myStruct, storedStruct1);
			CompareStructs(myStruct, storedStruct2);


		}

		private void CompareStructs(MyStruct myStruct, MyStruct storedStruct1)
		{
			Equal(myStruct.MyDateTime, storedStruct1.MyDateTime);
			//Equal(myStruct.Field, storedStruct1.Field);
			Equal(myStruct.Field1, storedStruct1.Field1);
			Equal(myStruct.Field2, storedStruct1.Field2);
			Equal(myStruct.MyGuid, storedStruct1.MyGuid);
			//Equal(myStruct.MyTimeSpan, storedStruct1.MyTimeSpan);
		}

//		[TestMethod]
//		public void Should_allow_update_one_of_property()
//		{
//			testObectState.Set("X", 1);
//			testObectState.Set("Y", 2);
//			sut.CreateObject(testObectState);
//			ReopenStorage();
//			testObectState.Set("X", 3);
//			testObectState.Set("Y", 4);
//			sut.LoadProperty(testObectState, "X");
//
//			Equal(1, testObectState.Get("X"));
//			Equal(4, testObectState.Get("Y"));
//		}

		[TestMethod]
		public void Should_allow_check_existence()
		{
			False(sut.IsExists(testObectState));
			sut.CreateObject(testObectState);
			True(sut.IsExists(testObectState));
			sut.DeleteObject(testObectState);
			False(sut.IsExists(testObectState));
		}
	}
}